/*
 * ============================================================================
 * GNU General Public License
 * ============================================================================
 *
 * Copyright (C) 2015 Infinite Automation Software. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * When signing a commercial license with Infinite Automation Software,
 * the following extension to GPL is made. A special exception to the GPL is
 * included to allow you to distribute a combined work that includes BAcnet4J
 * without being obliged to provide the source code for any proprietary components.
 *
 * See www.infiniteautomation.com for commercial license options.
 *
 * @author Matthew Lohbihler
 */
package org.thanos.bacnet.obj;

import cn.hutool.core.lang.Pair;
import org.thanos.bacnet.LocalDevice;
import org.thanos.bacnet.RemoteDevice;
import org.thanos.bacnet.ResponseConsumer;
import org.thanos.bacnet.apdu.AckAPDU;
import org.thanos.bacnet.enums.DayOfWeek;
import org.thanos.bacnet.exception.BACnetException;
import org.thanos.bacnet.exception.BACnetRuntimeException;
import org.thanos.bacnet.exception.BACnetServiceException;
import org.thanos.bacnet.obj.mixin.HasStatusFlagsMixin;
import org.thanos.bacnet.obj.mixin.event.IntrinsicReportingMixin;
import org.thanos.bacnet.obj.mixin.event.eventAlgo.NoneAlgo;
import org.thanos.bacnet.service.acknowledgement.AcknowledgementService;
import org.thanos.bacnet.service.confirmed.WritePropertyRequest;
import org.thanos.bacnet.type.Encodable;
import org.thanos.bacnet.type.constructed.BACnetArray;
import org.thanos.bacnet.type.constructed.CalendarEntry;
import org.thanos.bacnet.type.constructed.DailySchedule;
import org.thanos.bacnet.type.constructed.DateRange;
import org.thanos.bacnet.type.constructed.DateTime;
import org.thanos.bacnet.type.constructed.DeviceObjectPropertyReference;
import org.thanos.bacnet.type.constructed.DeviceObjectReference;
import org.thanos.bacnet.type.constructed.EventTransitionBits;
import org.thanos.bacnet.type.constructed.PropertyValue;
import org.thanos.bacnet.type.constructed.SequenceOf;
import org.thanos.bacnet.type.constructed.SpecialEvent;
import org.thanos.bacnet.type.constructed.StatusFlags;
import org.thanos.bacnet.type.constructed.TimeValue;
import org.thanos.bacnet.type.constructed.ValueSource;
import org.thanos.bacnet.type.enumerated.BinaryPV;
import org.thanos.bacnet.type.enumerated.ErrorClass;
import org.thanos.bacnet.type.enumerated.ErrorCode;
import org.thanos.bacnet.type.enumerated.EventState;
import org.thanos.bacnet.type.enumerated.NotifyType;
import org.thanos.bacnet.type.enumerated.ObjectType;
import org.thanos.bacnet.type.enumerated.PropertyIdentifier;
import org.thanos.bacnet.type.enumerated.Reliability;
import org.thanos.bacnet.type.primitive.Boolean;
import org.thanos.bacnet.type.primitive.Date;
import org.thanos.bacnet.type.primitive.Null;
import org.thanos.bacnet.type.primitive.ObjectIdentifier;
import org.thanos.bacnet.type.primitive.Primitive;
import org.thanos.bacnet.type.primitive.Time;
import org.thanos.bacnet.type.primitive.UnsignedInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * TODO
 * - use reliability to convey schedule problems.
 *
 * @author Matthew
 */
public class ScheduleObject extends BACnetObject {
	private static final Logger LOG = LoggerFactory.getLogger(ScheduleObject.class);

	private static final ReentrantLock LOCK = new ReentrantLock();
	private ScheduledFuture<?> presentValueRefersher;
	/**
	 * A proprietary mechanism to periodically write the present value to all property references in case of power
	 * failures, restarts, and the like.
	 */
	private ScheduledFuture<?> periodicWriter;

	public ScheduleObject(final LocalDevice localDevice, final int instanceNumber, final String name,
	                      final DateRange effectivePeriod, final BACnetArray<DailySchedule> weeklySchedule,
	                      final SequenceOf<SpecialEvent> exceptionSchedule, final Primitive scheduleDefault,
	                      final SequenceOf<DeviceObjectPropertyReference> listOfObjectPropertyReferences,
	                      final int priorityForWriting, final boolean outOfService) throws BACnetServiceException {
		super(localDevice, ObjectType.schedule, instanceNumber, name);

		if (effectivePeriod == null)
			throw new BACnetRuntimeException("effectivePeriod cannot be null");
		if (weeklySchedule == null && exceptionSchedule == null)
			throw new BACnetRuntimeException("Both weeklySchedule and exceptionSchedule cannot be null");
		if (scheduleDefault == null)
			throw new BACnetRuntimeException("scheduleDefault cannot be null");
		if (listOfObjectPropertyReferences == null)
			throw new BACnetRuntimeException("listOfObjectPropertyReferences cannot be null");

		writePropertyInternal(PropertyIdentifier.effectivePeriod, effectivePeriod);
		writePropertyInternal(PropertyIdentifier.scheduleDefault, scheduleDefault);
		if (weeklySchedule != null) {
			if (weeklySchedule.getCount() != 7)
				throw new BACnetRuntimeException("weeklySchedule must have 7 elements");
			writeProperty(null, new PropertyValue(PropertyIdentifier.weeklySchedule, weeklySchedule));
		}
		if (exceptionSchedule != null)
			writeProperty(null, new PropertyValue(PropertyIdentifier.exceptionSchedule, exceptionSchedule));
		writePropertyInternal(PropertyIdentifier.presentValue, scheduleDefault);
		writeProperty(null,
				new PropertyValue(PropertyIdentifier.listOfObjectPropertyReferences, listOfObjectPropertyReferences));
		writePropertyInternal(PropertyIdentifier.priorityForWriting, new UnsignedInteger(priorityForWriting));
		writePropertyInternal(PropertyIdentifier.reliability, Reliability.noFaultDetected);
		writePropertyInternal(PropertyIdentifier.outOfService, Boolean.valueOf(outOfService));
		writePropertyInternal(PropertyIdentifier.statusFlags, new StatusFlags(false, false, false, outOfService));

		addMixin(new HasStatusFlagsMixin(this));
		addMixin(new ScheduleMixin(this));

		final Primitive oldValue = get(PropertyIdentifier.presentValue);
		updatePresentValue();
		final Primitive newValue = get(PropertyIdentifier.presentValue);
		// If the present value didn't change after the update, then no write would have been done. So, to ensure
		// initialization of the objects in the list, force a write.
		if (Objects.equals(oldValue, newValue)) {
			DateTime now = new DateTime(getLocalDevice().getClock().millis());
			if (effectivePeriod.matches(now.getDate())) {
				getLocalDevice().execute(() -> doWrites(newValue));
			}
		}

		localDevice.addObject(this);
	}

	// CreateObject constructor
	public static ScheduleObject create(final LocalDevice localDevice, final int instanceNumber)
			throws BACnetServiceException {
		return new ScheduleObject(localDevice, instanceNumber, ObjectType.schedule + " " + instanceNumber,
				new DateRange(Date.UNSPECIFIED, Date.UNSPECIFIED),
				new BACnetArray<>(7, new DailySchedule(new SequenceOf<>())), new SequenceOf<>(), BinaryPV.inactive,
				new SequenceOf<>(), 12, false);
	}

	private static long nextDay(final DateTime now) {
		final GregorianCalendar gc = now.getGC();
		gc.add(Calendar.DATE, 1);
		gc.add(Calendar.HOUR_OF_DAY, -gc.get(Calendar.HOUR_OF_DAY));
		gc.add(Calendar.MINUTE, -gc.get(Calendar.MINUTE));
		gc.add(Calendar.SECOND, -gc.get(Calendar.SECOND));
		gc.add(Calendar.MILLISECOND, -gc.get(Calendar.MILLISECOND));
		return gc.getTimeInMillis();
	}

	private static long timeOf(final Date date, final TimeValue tv) {
		final DateTime dt = new DateTime(date, tv.getTime());
		return dt.getGC().getTimeInMillis();
	}

	public void supportIntrinsicReporting(final int notificationClass, final EventTransitionBits eventEnable,
	                                      final NotifyType notifyType) {
		// Prepare the object with all of the properties that intrinsic reporting will need.
		// User-defined properties
		writePropertyInternal(PropertyIdentifier.notificationClass, new UnsignedInteger(notificationClass));
		writePropertyInternal(PropertyIdentifier.eventEnable, eventEnable);
		writePropertyInternal(PropertyIdentifier.eventState, EventState.normal);
		writePropertyInternal(PropertyIdentifier.notifyType, notifyType);
		writePropertyInternal(PropertyIdentifier.eventDetectionEnable, Boolean.TRUE);

		// Now add the mixin.
		addMixin(new IntrinsicReportingMixin(this, new NoneAlgo(), null, null, new PropertyIdentifier[0]));
	}

	@Override
	protected boolean validateProperty(final ValueSource valueSource, final PropertyValue value)
			throws BACnetServiceException {
		if (PropertyIdentifier.listOfObjectPropertyReferences.equals(value.getPropertyIdentifier())) {
			// Entries must reference properties of this type
			final Primitive scheduleDefault = get(PropertyIdentifier.scheduleDefault);
			final SequenceOf<DeviceObjectPropertyReference> refs = value.getValue();
			for (final DeviceObjectPropertyReference ref : refs) {
				final ObjectPropertyTypeDefinition def = ObjectProperties.getObjectPropertyTypeDefinition(
						ref.getObjectIdentifier().getObjectType(), ref.getPropertyIdentifier());
				if (def != null) {
					if ((scheduleDefault.getClass() != Null.instance.getClass())
							&& (scheduleDefault.getClass() != def.getPropertyTypeDefinition().getClazz())) {
						throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidDataType);
					}
				}
			}
		} else if (PropertyIdentifier.weeklySchedule.equals(value.getPropertyIdentifier())) {
			// Time value entries must be of this type
			final Primitive scheduleDefault = get(PropertyIdentifier.scheduleDefault);
			final BACnetArray<DailySchedule> weeklySchedule = value.getValue();
			for (final DailySchedule daily : weeklySchedule) {
				for (final TimeValue timeValue : daily.getDaySchedule()) {
					if ((scheduleDefault.getClass() != Null.instance.getClass()
							&& timeValue.getValue().getClass() != Null.instance.getClass())
							&& (scheduleDefault.getClass() != timeValue.getValue().getClass())) {
						throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidDataType);
					}
					if (!timeValue.getTime().isFullySpecified()) {
						throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidConfigurationData);
					}
				}
			}
		} else if (PropertyIdentifier.exceptionSchedule.equals(value.getPropertyIdentifier())) {
			// Time value entries must be of this type
			final Primitive scheduleDefault = get(PropertyIdentifier.scheduleDefault);
			final SequenceOf<SpecialEvent> exceptionSchedule = value.getValue();
			for (final SpecialEvent specialEvent : exceptionSchedule) {
				for (final TimeValue timeValue : specialEvent.getListOfTimeValues()) {
					if ((scheduleDefault.getClass() != Null.instance.getClass()
							&& timeValue.getValue().getClass() != Null.instance.getClass())
							&& (scheduleDefault.getClass() != timeValue.getValue().getClass())) {
						throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidDataType);
					}
					if (!timeValue.getTime().isFullySpecified()) {
						throw new BACnetServiceException(ErrorClass.property, ErrorCode.invalidConfigurationData);
					}
				}
			}
		}
		return false;
	}

	/**
	 * Starts the internal periodic writer.
	 *
	 * @param delay  the delay before the first execution, in milliseconds.
	 * @param period the period between executions, in milliseconds.
	 */
	public void startPeriodicWriter(final long delay, final long period) {
		if (delay < 0)
			throw new IllegalArgumentException("delay cannot be < 0");
		if (period < 1)
			throw new IllegalArgumentException("period cannot be < 1");

		cancelPeriodicWriter();
		periodicWriter = getLocalDevice().scheduleAtFixedRate(this::forceWrites, delay, period,
				TimeUnit.MILLISECONDS);
		LOG.debug("Periodic writer started");
	}

	public void stopPeriodicWriter() {
		cancelPeriodicWriter();
	}

	@Override
	protected void terminateImpl() {
		cancelRefresher();
		cancelPeriodicWriter();
	}

	public synchronized void forceWrites() {
		doWrites(get(PropertyIdentifier.presentValue));
	}

	private void cancelRefresher() {
		if (presentValueRefersher != null) {
			presentValueRefersher.cancel(false);
			presentValueRefersher = null;
		}
	}

	private void cancelPeriodicWriter() {
		if (periodicWriter != null) {
			periodicWriter.cancel(false);
			periodicWriter = null;
		}
	}

	void updatePresentValue() {
		LOCK.lock();
		final GregorianCalendar gc = new GregorianCalendar();
		gc.setTimeInMillis(getLocalDevice().getClock().millis());
		updatePresentValue(new DateTime(gc));
		LOCK.unlock();
	}

	private void updatePresentValue(final DateTime now) {
		cancelRefresher();

		Primitive newValue;
		long nextCheck;

		final Primitive scheduleDefault = get(PropertyIdentifier.scheduleDefault);
		final DateRange effectivePeriod = get(PropertyIdentifier.effectivePeriod);
		if (!effectivePeriod.matches(now.getDate())) {
			// Not in the current effective date.
			nextCheck = nextDay(now);
		} else {
			// Is there an exception schedule in effect?
			final SpecialEvent specialEvent = findExceptionSchedule(now);
			final DailySchedule dailySchedule = findDailySchedule(now);
			if (specialEvent == null && dailySchedule == null) {
				newValue = scheduleDefault;
				nextCheck = nextDay(now);
			} else {
				Pair<TimeValue, TimeValue> specialTriple = null;
				Pair<TimeValue, TimeValue> dailyTriple = null;
				if (specialEvent != null) {
					specialTriple = findTimeValue(now, specialEvent.getListOfTimeValues());
				}
				if (dailySchedule != null) {
					dailyTriple = findTimeValue(now, dailySchedule.getDaySchedule());
				}
				Pair<TimeValue, TimeValue> timeValuePair
						= Pair.of(findCurrentTv(specialTriple, dailyTriple), findNextTv(specialTriple, dailyTriple));
				if (!validateTimeValuePairCurrent(timeValuePair)) {
					newValue = scheduleDefault;
				} else {
					newValue = timeValuePair.getKey().getValue();
					if (newValue.getClass() == Null.instance.getClass()) {
						newValue = scheduleDefault;
					}
				}
				if (!validateTimeValuePairNext(timeValuePair)) {
					nextCheck = nextDay(now);
				} else {
					nextCheck = timeOf(now.getDate(), timeValuePair.getValue());
				}
			}
			writePropertyInternal(PropertyIdentifier.presentValue, newValue);
		}

		final java.util.Date nextRuntime = new java.util.Date(nextCheck);
		long interval = nextRuntime.getTime() - System.currentTimeMillis();
		presentValueRefersher = getLocalDevice().schedule(this::updatePresentValue, interval, TimeUnit.MILLISECONDS);
		LOG.debug("Timer scheduled to run after {} seconds", interval / 1000);
	}


//    private void updatePresentValue(final DateTime now) {
//        cancelRefresher();
//
//        Primitive newValue;
//        long nextCheck;
//
//        final Primitive scheduleDefault = get(PropertyIdentifier.scheduleDefault);
//        final DateRange effectivePeriod = get(PropertyIdentifier.effectivePeriod);
//        if (!effectivePeriod.matches(now.getDate())) {
//            // Not in the current effective date.
//            newValue = scheduleDefault;
//            nextCheck = nextDay(now);
//        } else {
//            SequenceOf<TimeValue> schedule = null;
//
//            // Is there an exception schedule in effect?
//            final SpecialEvent specialEvent = findExceptionSchedule(now);
//            if (specialEvent != null)
//                schedule = specialEvent.getListOfTimeValues();
//            else {
//                final DailySchedule dailySchedule = findDailySchedule(now);
//                if (dailySchedule != null)
//                    schedule = dailySchedule.getDaySchedule();
//            }
//
//            if (schedule == null) {
//                newValue = scheduleDefault;
//                nextCheck = nextDay(now);
//            } else {
//                // Find the schedule entry in effect at this time.
//                TimeValue currentTv = null;
//                int tvIndex = schedule.getCount();
//                for (; tvIndex > 0; tvIndex--) {
//                    final TimeValue tv = schedule.getBase1(tvIndex);
//
//                    if (!tv.getTime().after(now.getTime())) {
//                        // Found a time value entry that can be used.
//                        currentTv = tv;
//                        break;
//                    }
//                }
//
//                // Determine the new present value.
//                if (currentTv == null)
//                    newValue = scheduleDefault;
//                else
//                    newValue = currentTv.getValue();
//
//                // Determine the next time this method should run.
//                if (tvIndex < schedule.getCount()) {
//                    final TimeValue nextTv = schedule.getBase1(tvIndex + 1);
//                    nextCheck = timeOf(now.getDate(), nextTv);
//                } else
//                    nextCheck = nextDay(now);
//            }
//        }
//
//        writePropertyInternal(PropertyIdentifier.presentValue, newValue);
//
//        final java.util.Date nextRuntime = new java.util.Date(nextCheck);
//        long interval = nextRuntime.getTime() - new java.util.Date().getTime();
//        presentValueRefersher = getLocalDevice().schedule(this::updatePresentValue, interval, TimeUnit.MILLISECONDS);
//        LOG.debug("Timer scheduled to run at {}", nextRuntime);
//    }

	private TimeValue findCurrentTv(Pair<TimeValue, TimeValue> specialTriple,
	                                Pair<TimeValue, TimeValue> dailyTriple) {
		if (!validateTimeValuePairCurrent(specialTriple) && !validateTimeValuePairCurrent(dailyTriple)) {
			return null;
		}
		if (validateTimeValuePairCurrent(specialTriple)) {
			if (specialTriple.getKey().getValue().getClass() != Null.instance.getClass()) {
				return specialTriple.getKey();
			}
		}
		return dailyTriple.getKey();
	}

	private TimeValue findNextTv(Pair<TimeValue, TimeValue> specialTriple,
	                             Pair<TimeValue, TimeValue> dailyTriple) {
		if (!validateTimeValuePairNext(specialTriple) && !validateTimeValuePairNext(dailyTriple)) {
			return null;
		}
		if (validateTimeValuePairCurrent(specialTriple)) {
			return specialTriple.getValue();
		}
		if (validateTimeValuePairNext(specialTriple) && !validateTimeValuePairNext(dailyTriple)) {
			return specialTriple.getValue();
		}
		if (validateTimeValuePairNext(specialTriple) && validateTimeValuePairNext(dailyTriple)) {
			if (dailyTriple.getValue().getTime().before(specialTriple.getValue().getTime())) {
				return dailyTriple.getValue();
			} else {
				return specialTriple.getValue();
			}
		} else {
			return Objects.isNull(dailyTriple) ? null : dailyTriple.getValue();
		}
	}

	private boolean validateTimeValuePairCurrent(Pair<TimeValue, TimeValue> pair) {
		return pair != null && pair.getKey() != null;
	}

	private boolean validateTimeValuePairNext(Pair<TimeValue, TimeValue> pair) {
		return pair != null && pair.getValue() != null;
	}

	private Pair<TimeValue, TimeValue> findTimeValue(final DateTime now, SequenceOf<TimeValue> schedule) {
		if (schedule == null) {
			return null;
		}
		TimeValue currentTv = null;
		TimeValue nextTv = null;
		int tvIndex = schedule.getCount();
		for (; tvIndex > 0; tvIndex--) {
			final TimeValue tv = schedule.getBase1(tvIndex);

			if (!tv.getTime().after(now.getTime())) {
				// Found a time value entry that can be used.
				currentTv = tv;
				break;
			}
		}
		if (tvIndex < schedule.getCount()) {
			nextTv = schedule.getBase1(tvIndex + 1);
		}
		return Pair.of(currentTv, nextTv);
	}

	private SpecialEvent findExceptionSchedule(final DateTime now) {
		final SequenceOf<SpecialEvent> exceptionSchedule = get(PropertyIdentifier.exceptionSchedule);
		if (exceptionSchedule == null)
			return null;

		SpecialEvent best = null;
		for (final SpecialEvent e : exceptionSchedule) {
			boolean active;
			if (e.isCalendarReference()) {
				final CalendarObject co = (CalendarObject) getLocalDevice().getObject(e.getCalendarReference());
				if (co != null) {
					// Getting the property this way ensures that the calendar's present value gets is calculated.
					try {
						final Boolean pv = co.readProperty(PropertyIdentifier.presentValue);
						active = pv.booleanValue();
					} catch (final BACnetServiceException ex) {
						LOG.warn("Error while retrieving calendar's present value", ex);
						active = false;
					}
				} else
					active = false;
			} else
				active = e.getCalendarEntry().matches(now.getDate());

			if (active) {
				int length = e.getListOfTimeValues().size() - 1;
				boolean inTimeRange = false;
				for (int i = 0; i < length; i = i + 2) {
					TimeValue timeValue1 = e.getListOfTimeValues().get(i);
					TimeValue timeValue2 = e.getListOfTimeValues().get(i + 1);
					if (now.getTime().before(timeValue2.getTime()) && now.getTime().after(timeValue1.getTime())) {
						inTimeRange = true;
						break;
					}
				}
				if (best == null || (best.getEventPriority().intValue() > e.getEventPriority().intValue())) {
					Map<CalendarEntry, SpecialEvent> entrySequenceOfMap = this.processSpecialEvent(exceptionSchedule);
					SpecialEvent specialEvent = entrySequenceOfMap.get(e.getCalendarEntry());
					if (Objects.nonNull(specialEvent) && !inTimeRange) {
						best = new SpecialEvent(e.getCalendarEntry(), specialEvent.getListOfTimeValues(), e.getEventPriority());
					} else {
						best = e;
					}
				}
			}
		}
		return best;
	}

	private Map<CalendarEntry, SpecialEvent> processSpecialEvent(SequenceOf<SpecialEvent> exceptionSchedule) {
		Map<CalendarEntry, SpecialEvent> tempMap = new HashMap<>();
		exceptionSchedule.getValues().forEach(specialEvent -> {
			SpecialEvent event = tempMap.get(specialEvent.getCalendarEntry());
			if (Objects.isNull(event)) {
				tempMap.put(specialEvent.getCalendarEntry(), specialEvent);
			} else {
				List<TimeValue> timeValues = event.getListOfTimeValues().getValues();
				for (TimeValue value : specialEvent.getListOfTimeValues().getValues()) {
					if (timeValues.stream().noneMatch(timeValue -> timeValue.getTime().equals(value.getTime()))) {
						timeValues.add(value);
					}
				}
				for (int i = 1; i < timeValues.size() - 1; i = i + 2) {
					TimeValue timeValue1 = timeValues.get(i);
					TimeValue timeValue2 = timeValues.get(i + 1);
					if (timeValue2.getTime().before(timeValue1.getTime()) && Null.instance.equals(timeValue1.getValue())) {
						Time time1 = timeValue1.getTime();
						Time time2 = timeValue2.getTime();
						timeValue1 = new TimeValue(time2, timeValue2.getValue());
						timeValue2 = new TimeValue(time1, timeValue2.getValue());
						timeValues.set(i, timeValue1);
						timeValues.set(i + 1, timeValue2);
					}
				}
				timeValues = timeValues.stream().collect(Collectors.toMap(TimeValue::getTime, // 使用姓名作为key
								p -> p, // 使用对象本身作为value
								(existing, replacement) -> existing)) // 如果有重复的key，则保留已存在的对象
						.values().stream().sorted(Comparator.comparing(TimeValue::getTime)).collect(Collectors.toList());
				tempMap.put(specialEvent.getCalendarEntry(), new SpecialEvent(specialEvent.getCalendarEntry(), new SequenceOf<>(timeValues), specialEvent.getEventPriority()));
			}
		});
		return tempMap;
	}

	private DailySchedule findDailySchedule(final DateTime now) {
		final BACnetArray<DailySchedule> weeklySchedule = get(PropertyIdentifier.weeklySchedule);
		if (weeklySchedule == null)
			return null;

		DayOfWeek dow = now.getDate().getDayOfWeek();
		if (!dow.isSpecific())
			dow = DayOfWeek.forDate(now.getDate());

		return weeklySchedule.getBase1(dow.getId());
	}

	void doWrites(final Encodable value) {
		final SequenceOf<DeviceObjectPropertyReference> listOfObjectPropertyReferences = get(
				PropertyIdentifier.listOfObjectPropertyReferences);
		final UnsignedInteger priorityForWriting = get(PropertyIdentifier.priorityForWriting);
		List<Integer> blackList = new ArrayList<>();
		// Send the write requests.
		for (final DeviceObjectPropertyReference dopr : listOfObjectPropertyReferences) {
			LOG.debug("Sending write request to {} in {}, value={}, priority={}", dopr.getObjectIdentifier(),
					dopr.getDeviceIdentifier(), value, priorityForWriting);

			if (dopr.getDeviceIdentifier() == null || Objects.equals(dopr.getDeviceIdentifier(), getLocalDevice().getId())) {
				// Local write.
				final BACnetObject that = getLocalDevice().getObject(dopr.getObjectIdentifier());
				try {
					that.writeProperty(new ValueSource(new DeviceObjectReference(getLocalDevice().getId(), getId())),
							new PropertyValue(dopr.getPropertyIdentifier(), dopr.getPropertyArrayIndex(), value,
									priorityForWriting));
				} catch (final BACnetServiceException e) {
					LOG.warn("Schedule failed to write to local object {}", dopr.getObjectIdentifier(), e);
				}
			} else {
				final ObjectIdentifier devId = dopr.getDeviceIdentifier();
				final ObjectIdentifier oid = dopr.getObjectIdentifier();
				if (blackList.contains(devId.getInstanceNumber())) {
					continue;
				}
				try {
					final RemoteDevice d = getLocalDevice().getRemoteDevice(devId.getInstanceNumber()).get();
					final WritePropertyRequest req = new WritePropertyRequest(oid, dopr.getPropertyIdentifier(),
							dopr.getPropertyArrayIndex(), value, priorityForWriting);
					getLocalDevice().send(d, req, new ResponseConsumer() {
						@Override
						public void success(final AcknowledgementService ack) {
							// Whatever.
						}

						@Override
						public void fail(final AckAPDU ack) {
							LOG.warn("Schedule failed to write to {} in {}, ack={}", oid, devId, ack);
						}

						@Override
						public void ex(final BACnetException e) {
							LOG.error("Schedule failed to write to {} in {}", oid, devId, e);
						}
					});
				} catch (final BACnetException e) {
					LOG.warn("Schedule failed to write to unknown remote device {}", devId, e);
					blackList.add(devId.getInstanceNumber());
				}
			}
		}
	}

	class ScheduleMixin extends AbstractMixin {
		public ScheduleMixin(final BACnetObject bo) {
			super(bo);
		}

		@Override
		protected boolean validateProperty(final ValueSource valueSource, final PropertyValue value)
				throws BACnetServiceException {
			if (PropertyIdentifier.presentValue.equals(value.getPropertyIdentifier())) {
				final Boolean outOfService = get(PropertyIdentifier.outOfService);
				if (!outOfService.booleanValue())
					throw new BACnetServiceException(ErrorClass.property, ErrorCode.writeAccessDenied);
			}
			return false;
		}

		@Override
		protected void afterWriteProperty(final PropertyIdentifier pid, final Encodable oldValue,
		                                  final Encodable newValue) {
			if (Objects.equals(newValue, oldValue))
				return;

			if (pid.isOneOf(PropertyIdentifier.effectivePeriod, PropertyIdentifier.weeklySchedule,
					PropertyIdentifier.exceptionSchedule, PropertyIdentifier.scheduleDefault))
				updatePresentValue();
			if (pid.equals(PropertyIdentifier.presentValue))
				doWrites(newValue);
		}
	}
}
